﻿//++
//
// Copyright (c) Microsoft Corporation.  All rights reserved.
//
// Module Name:
//
//  UpdateMonitorTask.cs
//    
// Abstract:
//
//  This module implements the UpdateMonitorTask class.
//
//--

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;

using Microsoft.OfficeCommunicationsServer.Applications.Common;

namespace Microsoft.EnhancedReminders.Scheduler
{
    internal class UpdateMonitorTask : ExchangeTask
    {
        // Search folder
        private BaseFolderIdType _searchFolderId;

        // Search expression used to create the search folder
        private readonly SearchExpressionType _searchExpression;

        // The parent of the search folder
        private readonly BaseFolderIdType _parentFolderId;

        // The target folder for the search expression
        private readonly BaseFolderIdType _targetFolderId;

        // Type of traversal used by the search folder
        private readonly SearchFolderTraversalType _traversal;

        // Subscription events of interested
        private readonly NotificationEventTypeType[] _eventTypes;

        // Name of the search folder
        private readonly string _name;

        // Subscription ID
        private Guid _subscriptionId;

        /// <summary>
        /// ctor
        /// </summary>
        /// <param name="taskContext">Task context</param>
        /// <param name="client">WCF proxy for communicating with EWS</param>
        /// <param name="impersonation">Exchange impersonation info</param>
        /// <param name="emailAddress">E-mail address of the user associated with this request</param>
        /// <param name="searchExpression">Search expression</param>
        /// <param name="searchFolderId">Existing search folder</param>
        /// <param name="parentFolderId">Parent of the search folder</param>
        /// <param name="targetFolderId">The folder in which to search</param>
        /// <param name="traversal">Type of traversal used by the search folder</param>
        /// <param name="eventTypes">Events to subscribe to</param>
        /// <param name="name">Name of the search folder</param>
        /// <param name="subscriptionId">Subscription ID</param>
        internal UpdateMonitorTask(TaskContext taskContext, ExchangeServicePortType client, ExchangeImpersonationType impersonation,
            string emailAddress, SearchExpressionType searchExpression, BaseFolderIdType searchFolderId, BaseFolderIdType parentFolderId, 
            BaseFolderIdType targetFolderId, SearchFolderTraversalType traversal, NotificationEventTypeType[] eventTypes, string name, 
            Guid subscriptionId)
            : base(taskContext, client, impersonation, emailAddress)
        {
            Debug.Assert(parentFolderId != null, "parentFolderId != null");
            Debug.Assert(targetFolderId != null, "targetFolderId != null");
            Debug.Assert(eventTypes != null, "eventTypes != null");
            Debug.Assert(!String.IsNullOrEmpty(name), "!String.IsNullOrEmpty(name)");
            Debug.Assert(subscriptionId != null, "subscriptionId != null");

            _searchExpression = searchExpression;
            _searchFolderId = searchFolderId;
            _parentFolderId = parentFolderId;
            _targetFolderId = targetFolderId;
            _traversal = traversal;
            _eventTypes = eventTypes;
            _name = name;
            _subscriptionId = subscriptionId;
        }

        /// <summary>
        /// Starts the update process.
        /// </summary>
        internal override void Begin()
        {
            Logger.WriteTraceVerbose(Context, "UpdateMonitorTask.Begin");

            if (_searchFolderId != null)
            {
                // If there is already a search folder, start by deleting the existing search folder
                BeginDeleteSearchFolder();
            }
            else 
            {
                // If there is a valid search expression, start by creating the search folder
                BeginCreateSearchFolderIfNeeded();
            }
        }

        /// <summary>
        /// Requests EWS to create a search folder if there's a valid search expression. 
        /// </summary>
        private void BeginCreateSearchFolderIfNeeded()
        {
            if (_searchExpression == null)
            {
                // If there's no search expression, then the task is now complete
                RaiseCompleted(new UpdateMonitorTaskCompletedEventArgs(_taskContext, null, null));
            }
            else
            {
                // Otherwise, create the search folder for the search expression associated with this task
                BeginCreateSearchFolder();
            }
        }

        /// <summary>
        /// Requests EWS to create a search folder that macthes the search expression associated with this task.
        /// </summary>
        private void BeginCreateSearchFolder()
        {
            Debug.Assert(_searchFolderId == null, "_searchFolder == null");
            Debug.Assert(_searchExpression != null, "_searchExpression != null");

            Logger.WriteTraceVerbose(Context, "UpdateMonitorTask.BeginCreateSearchFolder");

            RestrictionType restriction = new RestrictionType();
            restriction.Item = _searchExpression;

            // Initialize search folder
            SearchFolderType newFolder = new SearchFolderType();
            newFolder.DisplayName = _name;
            newFolder.SearchParameters = new SearchParametersType();
            newFolder.SearchParameters.Traversal = _traversal;
            newFolder.SearchParameters.TraversalSpecified = true;
            newFolder.SearchParameters.BaseFolderIds = new BaseFolderIdType[] { _targetFolderId };
            newFolder.SearchParameters.Restriction = restriction;

            CreateFolderType createFolder = new CreateFolderType();
            createFolder.ParentFolderId = new TargetFolderIdType();
            createFolder.ParentFolderId.Item = _parentFolderId;
            createFolder.Folders = new SearchFolderType[] { newFolder };

            CreateFolderRequest createFolderRequest = new CreateFolderRequest();
            createFolderRequest.CreateFolder = createFolder;
            createFolderRequest.ExchangeImpersonation = _impersonation;
            createFolderRequest.RequestServerVersion = ExchangeUsers.RequestServerVersion;

            try
            {
                _client.BeginCreateFolder(createFolderRequest, OnCreateSearchFolderCompleted, null);
            }
            catch (Exception ex)
            {
                HandleAccessError(ex, "BeginCreateFolder failed.");
            }
        }

        /// <summary>
        /// Callback invoked when the search folder has been created.
        /// </summary>
        /// <param name="asyncResult">Status of the asynchronous operation</param>
        /// <remarks>
        /// If the search folder has been created successfully, a new subscription is started for this folder. Otherwise, 
        /// the update is terminated with the appropriate error.
        /// </remarks>
        private void OnCreateSearchFolderCompleted(IAsyncResult asyncResult)
        {
            Logger.WriteTraceVerbose(Context, "UpdateMonitorTask.OnCreateSearchFolderCompleted");

            CreateFolderResponse createFolderResponse = GetResponse<CreateFolderResponse>(asyncResult, _client.EndCreateFolder, "EndCreateFolder failed.");

            if (createFolderResponse == null)
            {
                return;
            }

            FolderInfoResponseMessageType responseMessage = 
                Helpers.AssertCast<FolderInfoResponseMessageType>(createFolderResponse.CreateFolderResponse1.ResponseMessages.Items[0]);
            Debug.Assert(responseMessage != null, "responseMessage != null");

            if (responseMessage.ResponseCode == ResponseCodeType.NoError && responseMessage.Folders.Length == 1)
            {
                // Search folder created successfully
                Logger.WriteTraceInformation(Context, "Search folder created successfully.");
                _searchFolderId = responseMessage.Folders[0].FolderId;
                Subscribe();
            }
            else if (responseMessage.ResponseCode == ResponseCodeType.ErrorFolderExists)
            {
                // The search folder already exists so it needs to be delete before creating a new one
                Logger.WriteTraceInformation(Context, "Search folder already exists.");
                BeginFindSearchFolder();
            }
            else
            {
                // Update task failed
                Logger.WriteTraceWarning(Context, "Failed to create search folder. Response Code: {0}", responseMessage.ResponseCode);
                RaiseCompleted(new UpdateMonitorTaskCompletedEventArgs(_taskContext, null, null,
                    new ExchangeTaskFailedException(Context, responseMessage.ResponseCode, "Failed to create search folder")));
            }
        }

        /// <summary>
        /// Requests EWS for the search folder associated with for this task.
        /// </summary>
        /// <remarks>
        /// This is only called at start-up to find any orphaned search folders. The folder is identified by name and parent folder.
        /// </remarks>
        private void BeginFindSearchFolder()
        {
            Debug.Assert(_searchFolderId == null, "_searchFolder == null");

            Logger.WriteTraceVerbose(Context, "UpdateMonitorTask.BeginFindSearchFolder");

            // The search folder is identified by its name
            ConstantValueType value = new ConstantValueType();
            value.Value = _name;

            FieldURIOrConstantType constant = new FieldURIOrConstantType();
            constant.Item = value;

            PathToUnindexedFieldType folderDisplayName = new PathToUnindexedFieldType();
            folderDisplayName.FieldURI = UnindexedFieldURIType.folderDisplayName;

            IsEqualToType equals = new IsEqualToType();
            equals.Item = folderDisplayName;
            equals.FieldURIOrConstant = constant;

            RestrictionType restriction = new RestrictionType();
            restriction.Item = equals;

            FindFolderType findFolder = new FindFolderType();
            findFolder.Traversal = FolderQueryTraversalType.Shallow;
            findFolder.FolderShape = new FolderResponseShapeType();
            findFolder.FolderShape.BaseShape = DefaultShapeNamesType.IdOnly;
            findFolder.ParentFolderIds = new BaseFolderIdType[] { _parentFolderId };
            findFolder.Restriction = restriction;

            FindFolderRequest findRequest = new FindFolderRequest();
            findRequest.FindFolder = findFolder;
            findRequest.ExchangeImpersonation = _impersonation;
            findRequest.RequestServerVersion = ExchangeUsers.RequestServerVersion;

            try
            {
                // Find the specified folder via EWS
                _client.BeginFindFolder(findRequest, OnFindSearchFolderCompleted, null /* asyncState */);
            }
            catch (Exception ex)
            {
                HandleAccessError(ex, "BeginFindFolder failed.");
            }
        }

        /// <summary>
        /// Callback invoked when the search folder is found.
        /// </summary>
        /// <param name="asyncResult">Status of the asynchronous operation</param>
        /// <remarks>
        /// If the search folder was found, the folder is deleted (following which a new search folder will be created). If the 
        /// search folder can't be found, a new search folder is created immediately. A search folder should only be found if a
        /// previous instance of the Scheduler service did not shut down cleanly.
        /// </remarks>
        private void OnFindSearchFolderCompleted(IAsyncResult asyncResult)
        {
            Logger.WriteTraceVerbose(Context, "UpdateMonitorTask.OnFindSearchFolderCompleted");

            FindFolderResponse findFolderResponse = GetResponse<FindFolderResponse>(asyncResult, _client.EndFindFolder, "EndFindFolder failed.");

            if (findFolderResponse == null)
            {
                return;
            }

            FindFolderResponseMessageType responseMessage = 
                Helpers.AssertCast<FindFolderResponseMessageType>(findFolderResponse.FindFolderResponse1.ResponseMessages.Items[0]);
            Debug.Assert(responseMessage != null, "responseMessage != null");

            if (responseMessage.ResponseCode == ResponseCodeType.NoError && responseMessage.RootFolder.Folders.Length == 1)
            {
                // Found an existing search folder so now delete it
                Logger.WriteTraceInformation(Context, "Found pre-existing search folder.");
                _searchFolderId = responseMessage.RootFolder.Folders[0].FolderId;
                BeginDeleteSearchFolder();
            }
            else
            {
                // Did not find a pre-exsisting search folder so create a search folder (if there's a valid search expression)
                Logger.WriteTraceInformation(Context, "Did not find pre-existing search folder.");
                BeginCreateSearchFolderIfNeeded();
            }
        }

        /// <summary>
        /// Requests EWS to delete the search folder associated with this task.
        /// </summary>
        private void BeginDeleteSearchFolder()
        {
            Debug.Assert(_searchFolderId != null, "_searchFolder != null");

            Logger.WriteTraceVerbose(Context, "UpdateMonitorTask.BeginDeleteSearchFolder");

            DeleteFolderTask deleteFolderTask = new DeleteFolderTask(_taskContext, _client, _impersonation, _emailAddress, _searchFolderId);
            deleteFolderTask.Completed += DeleteFolderTask_Completed;
            deleteFolderTask.Begin();
        }

        /// <summary>
        /// Handles completion of the search folder deletion task.
        /// </summary>
        /// <param name="sender">Delete folder task</param>
        /// <param name="e">Delete folder task completed event args</param>
        void DeleteFolderTask_Completed(object sender, TaskCompletedEventArgs e)
        {
            Debug.Assert(sender != null, "sender != null");
            Debug.Assert(e != null, "e != null");
            Debug.Assert(e.Context != null, "e.Context != null");
            Debug.Assert(e.Context == Context, "e.Context == Context");

            Logger.WriteTraceVerbose(Context, "UpdateMonitorTask.DeleteFolderTask_Completed");

            sender.AssertCast<DeleteFolderTask>().Completed -= DeleteFolderTask_Completed;

            if (e.Error != null)
            {
                // Update task failed. Assume the search folder still exists.
                RaiseCompleted(new UpdateMonitorTaskCompletedEventArgs(e.TaskContext, _searchFolderId, null, e.Error));
                return;
            }

            // Search folder has been deleted
            _searchFolderId = null;

            // Create a new search folder (if there's a valid search expression)
            BeginCreateSearchFolderIfNeeded();
        }

        /// <summary>
        /// Subscribes for notifications from EWS for changes to the search folder associated with this task.
        /// </summary>
        private void Subscribe()
        {
            Debug.Assert(_searchFolderId != null, "_searchFolder != null");

            Logger.WriteTraceVerbose(Context, "UpdateMonitorTask.Subscribe");

            SubscribeTask subscribeTask = new SubscribeTask(_taskContext, _client, _impersonation, _emailAddress, _searchFolderId, _eventTypes, _subscriptionId, null);
            subscribeTask.Completed += SubscribeTask_Completed;
            subscribeTask.Begin();
        }

        /// <summary>
        /// Handles completion of the subscription task.
        /// </summary>
        /// <param name="sender">Subscribe task</param>
        /// <param name="e">Subscribe task completed event args</param>
        private void SubscribeTask_Completed(object sender, TaskCompletedEventArgs e)
        {
            Debug.Assert(sender != null, "sender != null");
            Debug.Assert(e != null, "e != null");
            Debug.Assert(e.Context != null, "e.Context != null");
            Debug.Assert(e.Context == Context, "e.Context == Context");

            Logger.WriteTraceVerbose(Context, "UpdateMonitorTask.SubscribeTask_Completed");
            
            sender.AssertCast<SubscribeTask>().Completed -= SubscribeTask_Completed;

            if (e.Error != null)
            {
                // Update task failed
                RaiseCompleted(new UpdateMonitorTaskCompletedEventArgs(e.TaskContext, _searchFolderId, null, e.Error));
                return;
            }

            // Find all the mailbox items in the newly created search folder
            FindItemTask findItemTask = new FindItemTask(_taskContext, _client, _impersonation, _emailAddress, 
                null /* find all items in the folder */, _searchFolderId);
            findItemTask.Completed += FindItemTask_Completed;
            findItemTask.Begin();
        }

        /// <summary>
        /// Handles completion of the find items task.
        /// </summary>
        /// <param name="sender">Find items task</param>
        /// <param name="e">Find items task completed event args</param>
        private void FindItemTask_Completed(object sender, TaskCompletedEventArgs e)
        {
            Debug.Assert(sender != null, "sender != null");
            Debug.Assert(e != null, "e != null");
            Debug.Assert(e.Context != null, "e.Context != null");
            Debug.Assert(e.Context == Context, "e.Context == Context");

            Logger.WriteTraceVerbose(Context, "UpdateMonitorTask.FindItemTask_Completed");

            sender.AssertCast<FindItemTask>().Completed -= FindItemTask_Completed;

            if (e.Error != null)
            {
                // Update task failed
                RaiseCompleted(new UpdateMonitorTaskCompletedEventArgs(e.TaskContext, _searchFolderId, null, e.Error));
                return;
            }

            // Update task completed successfully
            RaiseCompleted(new UpdateMonitorTaskCompletedEventArgs(_taskContext, _searchFolderId, e.AssertCast<FindItemTaskCompletedEventArgs>().Items));
        }
    }
}
